/*
 * Copyright 2011 Kim Lindhardt Madsen
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package dk.lindhardt.gwt.geie.client.evaluate;

import dk.lindhardt.gwt.geie.shared.GeieCalculationException;
import org.matheclipse.parser.client.ast.ASTNode;
import org.matheclipse.parser.client.ast.FunctionNode;
import org.matheclipse.parser.client.ast.NumberNode;
import org.matheclipse.parser.client.ast.SymbolNode;
import org.matheclipse.parser.client.eval.BooleanVariable;
import org.matheclipse.parser.client.eval.DoubleNode;
import org.matheclipse.parser.client.eval.DoubleVariable;
import org.matheclipse.parser.client.eval.IDoubleValue;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
 * Evaluate math expressions to either String, Double, Boolean or Date values.
 *
 */
public class GeieEvaluator {

   public interface RangeBuilder {
      Range buildRange(String left, String right);
   }

   private Map<String, BooleanVariable> booleanVariableMap;
   private Map<String, DoubleVariable> doubleVariableMap;
   private Map<String, DateVariable> dateVariableMap;
   private Map<String, StringVariable> stringVariableMap;
   private GeieBooleanEvaluator booleanEvaluator;
   private GeieDoubleEvaluator doubleEvaluator;
   private GeieDateEvaluator dateEvaluator;
   private RangeBuilder rangeBuilder;

   public GeieEvaluator(RangeBuilder rangeBuilder) {
      this.doubleVariableMap = new HashMap<String, DoubleVariable>();
      this.booleanVariableMap = new HashMap<String, BooleanVariable>();
      this.dateVariableMap = new HashMap<String, DateVariable>();
      this.stringVariableMap = new HashMap<String, StringVariable>();
      this.doubleEvaluator = new GeieDoubleEvaluator(this);
      this.booleanEvaluator = new GeieBooleanEvaluator(this);
      this.dateEvaluator = new GeieDateEvaluator(this);
      this.rangeBuilder = rangeBuilder;
   }

   /**
    * Evaluate an already parsed in <code>FunctionNode</code> into a
    * <code>Serializable</code> value.
    *
    * @param functionNode evaluate a function node
    * @return the evaluated value
    *
    * @throws ArithmeticException
    *           if the <code>functionNode</code> cannot be evaluated.
    */
   public Serializable evaluateFunction(final FunctionNode functionNode) {
      if (functionNode.size() > 0 && functionNode.getNode(0) instanceof SymbolNode) {
         String symbol = functionNode.getNode(0).toString().toLowerCase();
         if (symbol.equals("if")) {
            if (functionNode.size() == 3) {
               if (Boolean.TRUE.equals(evaluateNode(functionNode.getNode(1)))) {
                  return evaluateNode(functionNode.getNode(2));
               } else {
                  return Boolean.FALSE;
               }
            } else if (functionNode.size() == 4) {
               if (Boolean.TRUE.equals(evaluateNode(functionNode.getNode(1)))) {
                  return evaluateNode(functionNode.getNode(2));
               } else {
                  return evaluateNode(functionNode.getNode(3));
               }
            }
         } else {
            if (GeieDoubleEvaluator.FUNCTION_DOUBLE_MAP.containsKey(symbol)) {
               return doubleEvaluator.evaluateFunction(functionNode);
            } else if (GeieBooleanEvaluator.FUNCTION_BOOLEAN_MAP.containsKey(symbol)) {
               return booleanEvaluator.evaluateFunction(functionNode);
            } else if (GeieDateEvaluator.FUNCTION_DATE_MAP.containsKey(symbol)) {
               return dateEvaluator.evaluateFunction(functionNode);
            }
         }
      }
      throw new ArithmeticException("Evaluating not possible for: " + functionNode.toString());
   }

   /**
    * Evaluate an already parsed in abstract syntax tree node into a
    * <code>Serializable</code> value.
    *
    * @param childNode
    *          abstract syntax tree node
    *
    * @return the evaluated value
    *
    * @throws ArithmeticException
    *           if the <code>node</code> cannot be evaluated.
    */
   public Serializable evaluateNode(final ASTNode childNode) {
      if (childNode instanceof DoubleNode) {
         return ((DoubleNode) childNode).doubleValue();
      }
      if (childNode instanceof FunctionNode) {
         FunctionNode childFunction = (FunctionNode) childNode;
         String childSymbol = childFunction.get(0).getString().toLowerCase();
         // Ranges is treated specially
         if ("colon".equals(childSymbol)) {
                  return rangeBuilder.buildRange(
                        childFunction.get(1).getString(),
                        childFunction.get(2).getString());
         } else {
            return evaluateFunction(childFunction);
         }
      }
      String nodeString = childNode.toString();
      if (childNode instanceof SymbolNode) {
         DoubleVariable dv = doubleVariableMap.get(nodeString);
         if (dv != null) {
            return dv.getValue();
         }
         Double dbl = GeieDoubleEvaluator.SYMBOL_DOUBLE_MAP.get(nodeString);
         if (dbl != null) {
            return dbl;
         }
         BooleanVariable bv = booleanVariableMap.get(nodeString);
         if (bv != null) {
            return bv.getValue();
         }
         Boolean bol = GeieBooleanEvaluator.SYMBOL_BOOLEAN_MAP.get(nodeString);
         if (bol != null) {
            return bol;
         }
         DateVariable dav = dateVariableMap.get(nodeString);
         if (dav != null) {
            return dav.getValue();
         }
         StringVariable sv = stringVariableMap.get(nodeString);
         if (sv != null) {
            return sv.getValue();
         }
      } else if (childNode instanceof NumberNode) {
         return ((NumberNode) childNode).doubleValue();
      }

      throw new ArithmeticException(
            "Evaluating not possible for: " + nodeString);
   }

   /**
    * Define a value for a given variable name.
    *
    * @param variableName
    * @param value
    */
   public void defineVariable(String variableName, DoubleVariable value) {
      doubleVariableMap.put(variableName, value);
   }

   /**
    * Define a value for a given variable name.
    *
    * @param variableName
    * @param value
    */
   public void defineVariable(String variableName, BooleanVariable value) {
      booleanVariableMap.put(variableName, value);
   }

   /**
    * Define a value for a given variable name.
    *
    * @param variableName
    * @param value
    */
   public void defineVariable(String variableName, DateVariable value) {
      dateVariableMap.put(variableName, value);
   }

   /**
    * Define a value for a given variable name.
    *
    * @param variableName
    * @param value
    */
   public void defineVariable(String variableName, StringVariable value) {
      stringVariableMap.put(variableName, value);
   }

   /**
    * Returns the double variable value to which the specified variableName is
    * mapped, or {@code null} if this map contains no mapping for the
    * variableName.
    *
    * @param variableName
    * @return
    */
   public IDoubleValue getDoubleVariable(String variableName) {
      return doubleVariableMap.get(variableName);
   }

   /**
    * Clear all defined variables for this evaluator.
    */
   public void clearVariables() {
      doubleVariableMap.clear();
      booleanVariableMap.clear();
      dateVariableMap.clear();
      stringVariableMap.clear();
   }


}
